"""Classes and methods for dealing with gtf formatted files.

    From http://mblab.wustl.edu/GTF22.html:

    GTF stands for Gene transfer format. It borrows from GFF, but has additional 
    structure that warrants a separate definition and format name.
    Structure is as GFF, so the fields are:
    <seqname> <source> <feature> <start> <end> <score> <strand> <frame> [attributes] [comments] 

    The default GTF version is 2.2.
    
"""

import string, sys, re, copy, types, collections
import Intervals
#import Genomics
#import GFF
#import IndexedGenome

class Error(Exception):
    """Base class for exceptions in this module."""
    def __str__(self):
        return str(self.message)
    def _get_message(self, message): return self._message
    def _set_message(self, message): self._message = message
    message = property(_get_message, _set_message)

class ParsingError(Error):
    """Exception raised for errors in the input.

    Attributes:
        message -- explanation of the error
    """

    def __init__(self, message):
        self.message = message

class gtf_entry:
    """read/write gtf formatted entry.

    The coordinates are kept internally in python coordinates (0-based, open-closed), but are
    output as inclusive 1-based coordinates according to

    http://www.sanger.ac.uk/Software/formats/GFF/
    """

    def __init__(self):
        self.mContig = "."
        self.mSource = "."
        self.mFeature = "."
        self.mFrame = "."
        self.mStart = 0
        self.mEnd = 0
        self.mScore = "."
        self.mStrand = "."
        self.mGeneId = None
        self.mTranscriptId =  None
        self.mAttributes = {}

    def read( self, line ):
        """read gff entry from line.
        
        <seqname> <source> <feature> <start> <end> <score> <strand> <frame> [attributes] [comments]
        """
        
        data = line[:-1].split("\t")

        try:
            (self.mContig, self.mSource, self.mFeature,
             self.mStart, self.mEnd, self.mScore, self.mStrand,
             self.mFrame ) = data[:8]
        except ValueError:
            raise ValueError( "parsing error in line `%s`" % line )

        ## note: frame might be .
        (self.mStart, self.mEnd) = map(int, (self.mStart, self.mEnd))
        self.mStart -= 1

        self.parseInfo( data[8], line )

    def parseInfo( self, attributes, line ):
        """parse attributes.
        """
        # remove comments
        attributes = attributes.split( "#" )[0]
        # separate into fields
        fields = map( lambda x: x.strip(), attributes.split(";")[:-1])
        self.mAttributes = {}
        
        for f in fields:
            
            d = map( lambda x: x.strip(), f.split(" "))
            
            n,v = d[0], d[1]
            if len(d) > 2: v = d[1:]

            if v[0] == '"' and v[-1] == '"':
                v = v[1:-1]
            else:
                ## try to convert to a value
                try:
                    v = float( v )
                    v = int( v )
                except ValueError:
                    pass
                except TypeError:
                    pass
                
            if n == "gene_id": 
                self.mGeneId = v     
            elif n == "transcript_id": 
                self.mTranscriptId = v
            else: 
                self.mAttributes[n] = v

        if not self.mGeneId:
            raise ParsingError( "missing attribute 'gene_id' in line %s" % line)
        if not self.mTranscriptId:
            raise ParsingError( "missing attribute 'transcript_id' in line %s" % line)

    def getAttributeField( self, full = True ):
        aa = []
        for k,v in self.mAttributes.items():
            if type(v) == types.StringType:
                aa.append( '%s "%s"' % (k,v) )
            else:
                aa.append( '%s %s' % (k,str(v)) )

        if full:
            return"; ".join( ['gene_id "%s"' % self.mGeneId, 
                                     'transcript_id "%s"' % self.mTranscriptId ] +\
                                        aa )
        else:
            return "; ".join( aa )

        return attributes

    def __cmp__(self, other):
        ## note: does compare by strand as well!
        return cmp( (self.mContig, self.mStrand, self.mStart),
                    (other.mContig, other.mStrand, other.mStart))

    def __str__(self):
        
        attributes = self.getAttributeField( )
        return "\t".join(map(str, (self.mContig, self.mSource,
                                     self.mFeature, 
                                     self.mStart + 1, self.mEnd,
                                     self.mScore, self.mStrand, self.mFrame,
                                     attributes))) + ";" 

    def invert( self, lcontig ):
        """invert genomic coordinates from
        forward to reverse coordinates and back.
        """

        if self.mStrand in ("-", "0", "-1"):
            x = min(self.mStart, self.mEnd)
            y = max(self.mStart, self.mEnd)
                
            self.mStart = lcontig - y
            self.mEnd   = lcontig - x

    def fromGFF( self, other, gene_id, transcript_id ):
        """fill from other entry."""
        self.mContig = other.mContig
        self.mSource = other.mSource
        self.mFeature = other.mFeature
        self.mStart = other.mStart
        self.mEnd = other.mEnd
        self.mScore = other.mScore
        self.mStrand = other.mStrand
        self.mFrame = other.mFrame
        self.mGeneId = gene_id
        self.mTranscriptId = transcript_id
        return self

    def copy( self, other ):
        """fill from other entry."""
        self.mContig = other.mContig
        self.mSource = other.mSource
        self.mFeature = other.mFeature
        self.mStart = other.mStart
        self.mEnd = other.mEnd
        self.mScore = other.mScore
        self.mStrand = other.mStrand
        self.mFrame = other.mFrame
        self.mGeneId = other.mGeneId
        self.mTranscriptId = other.mTranscriptId
        self.mAttributes = copy.copy(other.mAttributes)
        return self

    def clearAttributes( self ):
        self.mAttributes = {}

    def addAttribute( self, key, value = None):
        self.mAttributes[key] = value

    def __setitem__(self, key, value):
        self.addAttribute( key, value )
        
    def __getitem__(self, key):
        return self.mAttributes[key]

    def __contains__(self, key):
        return key in self.mAttributes
        
    def hasOverlap( self, other, min_overlap = 0 ):
        """returns true, if overlap with other entry.
        """
        return (self.mContig == other.mContig and self.mStrand == other.mStrand and \
                    min(self.mEnd, other.mEnd) - max(self.mStart, other.mStart) > min_overlap)

    def isIdentical( self, other, max_slippage = 0 ):
        """returns true, if self and other overlap completely.
        """
        return (self.mContig == other.mContig and \
                    self.mStrand == other.mStrand and \
                    abs(self.mEnd - other.mEnd) < max_slippage and \
                    abs(self.mStart - other.mStart < max_slippage) )

    def isHalfIdentical( self, other, max_slippage = 0 ):
        """returns true, if self and other overlap.
        """
        return (self.mContig == other.mContig and \
                    self.mStrand == other.mStrand and \
                    ( abs(self.mEnd - other.mEnd) < max_slippage or \
                          abs(self.mStart - other.mStart < max_slippage) ) )



def iterator( infile ):
    """return a simple iterator over all entries in a file."""
    while 1:
        line = infile.readline()
        if not line: raise StopIteration
        if line.startswith("#"): continue
        gff = gtf_entry()
        gff.read( line )
        yield gff
